package bigbrother.log.daylog;

import bigbrother.ParameterChecker;
import bigbrother.log.logentries.LogEntry;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.BaseException;
import org.joda.time.DateTime;
import org.joda.time.ReadableDateTime;

import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;

/**
 * @author Hans Haggstrom
 */
public final class DayLogImpl
        implements DayLog
{

    //======================================================================
    // Private Fields

    private final DayLogId myId;
    private final File myLogStorageDirectory;

    private ReadableDateTime myHighestTimestamp;

    private List<LogEntry> myEntries;

    private transient XStream myXstream = new XStream();

    //======================================================================
    // Private Constants

    private static final Logger theLogger = Logger.getLogger( DayLogImpl.class.getName() );

    //======================================================================
    // Public Methods

    //----------------------------------------------------------------------
    // Constructors

    /**
     * Loads a {@link DayLog} from an existing file, for the specified day. If no file is found, a new empty {@link
     * DayLog} is created.
     *
     * @param id                  the {@link DayLogId} of the {@link DayLog} that should be retrieved.
     * @param logStorageDirectory the directory under which irc logs are saved.
     */
    public DayLogImpl( final DayLogId id, File logStorageDirectory )
    {
        ParameterChecker.checkNotNull( id, "id" );
        ParameterChecker.checkNotNull( logStorageDirectory, "logStorageDirectory" );

        myId = id;
        myLogStorageDirectory = logStorageDirectory;

        myHighestTimestamp = myId.getDate();

        myEntries = loadDayLogEntries();
    }

    //----------------------------------------------------------------------
    // DayLog Implementation

    public final DayLogId getId()
    {
        return myId;
    }


    public synchronized List<LogEntry> getEntries()
    {
        // Return a new copy of the entries to avoid problems if entries are added and concurrent access happens
        // TODO: This might be possible to optimize?  How to do it in a thread safe way?
        // The list has been kept unmodifiable to more easily allow later optimizations.
        return Collections.unmodifiableList( new ArrayList<LogEntry>( myEntries ) );
    }


    public synchronized void addEntry( LogEntry logEntry )
    {
        System.out.println( "DayLogImpl.addEntry" );

        checkLogEntryValidForThisDayLog( logEntry );

        myEntries.add( logEntry );

        // Check wether the entry is last, or in between other entries
        if ( logEntryIsLast( logEntry ) )
        {
            // Append the entry to the file
            appendLogEntry( logEntry );
        }
        else
        {
            // Put the entry in the correct place
            Collections.sort( myEntries );

            // Re-write the log file
            writeLogEntries( myEntries );

            // Update highest time stamp
            myHighestTimestamp = logEntry.getTimeStamp();
        }
    }

    //======================================================================
    // Private Methods

    private boolean logEntryIsLast( final LogEntry logEntry )
    {
        return logEntry.getTimeStamp().compareTo( myHighestTimestamp ) >= 0;
    }


    private void appendLogEntry( final LogEntry entry )
    {
        System.out.println( "DayLogImpl.appendLogEntry" );

        // TODO: Can we somehow remove / extract all this file handling broilerplate?
        // Maybe some interface method that just takes a writer and writes to it?

        final File fileLocation = getId().getLogFileLocation( myLogStorageDirectory );

        // Make directory structure if it doesn't exist
        fileLocation.getParentFile().mkdirs();

        BufferedWriter writer = null;
        try
        {
            // Open file for appending
            writer = new BufferedWriter( new FileWriter( fileLocation, true ) );

            // Append entry
            writeLogEntry( entry, writer );
        }
        catch ( IOException e )
        {
            theLogger.severe( "Could not append LogEntry to file '" + fileLocation + "'. " + e.getMessage() );
        }
        finally
        {
            try
            {
                if ( writer != null )
                {
                    writer.close();
                }
            }
            catch ( IOException e )
            {
                theLogger.severe( "Could not close DayLog file '" + fileLocation + "'. " + e.getMessage() );
            }
        }
    }


    private void writeLogEntries( final List<LogEntry> entries )
    {
        final File fileLocation = getId().getLogFileLocation( myLogStorageDirectory );

        // Make directory structure if it doesn't exist
        fileLocation.getParentFile().mkdirs();

        BufferedWriter writer = null;
        try
        {
            // Open file for rewriting
            writer = new BufferedWriter( new FileWriter( fileLocation, false ) );

            // Write entries
            for ( LogEntry entry : entries )
            {
                writeLogEntry( entry, writer );
            }
        }
        catch ( IOException e )
        {
            theLogger.severe( "Could not write DayLog to file '" + fileLocation + "'. " + e.getMessage() );
        }
        finally
        {
            try
            {
                if ( writer != null )
                {
                    writer.close();
                }
            }
            catch ( IOException e )
            {
                theLogger.severe( "Could not close DayLog file '" + fileLocation + "'. " + e.getMessage() );
            }
        }
    }


    private void checkLogEntryValidForThisDayLog( final LogEntry logEntry )
    {
        ParameterChecker.checkNotNull( logEntry, "logEntry" );

        if ( !logEntry.getTimeStamp().toDateMidnight().equals( myId.getDate() ) )
        {
            throw new IllegalArgumentException( "The timestamp of the added log entry is incorrect.  " +
                                                "It is '" + logEntry.getTimeStamp() + "', " +
                                                "but the date for this DayLog is '" + myId.getDate() + "' " );
        }

        if ( !logEntry.getDayLogId().equals( myId ) )
        {
            throw new IllegalArgumentException( "The DayLog id of the added log entry is incorrect.  " +
                                                "It is '" + logEntry.getDayLogId() + "', " +
                                                "but the id for this DayLog is '" + myId + "' " );
        }
    }


    /**
     * Tries to load log data from the given file.
     *
     * @return the read log entries in the correct order.
     */
    private List<LogEntry> loadDayLogEntries()
    {
        final List<LogEntry> entries = new ArrayList<LogEntry>();

        final File fileLocation = getId().getLogFileLocation( myLogStorageDirectory );
        BufferedReader reader = null;
        try
        {
            // Open file for reading
            reader = new BufferedReader( new FileReader( fileLocation ) );

            // Read entries
            myHighestTimestamp = myId.getDate();
            LogEntry logEntry;
            //noinspection NestedAssignment
            while ( ( logEntry = readLogEntry( reader ) ) != null )
            {
                entries.add( logEntry );

                // Update highest time stamp
                final DateTime entryTime = logEntry.getTimeStamp();
                if ( entryTime.isAfter( myHighestTimestamp ) )
                {
                    myHighestTimestamp = entryTime;
                }
            }

            // Make sure entries are correctly sorted
            Collections.sort( myEntries );
        }
        catch ( IOException e )
        {
            theLogger.severe( "Could not load DayLog from file '" + fileLocation + "'. " + e.getMessage() );
        }
        finally
        {
            try
            {
                if ( reader != null )
                {
                    reader.close();
                }
            }
            catch ( IOException e )
            {
                theLogger.severe( "Could not close DayLog file '" + fileLocation + "'. " + e.getMessage() );
            }
        }

        return entries;
    }

    // TODO: Split serialization and deserialiation into a separate strategy, to allow import of text logs and such too.

    private void writeLogEntry( final LogEntry entry, final BufferedWriter writer ) throws IOException
    {
        System.out.println( "DayLogImpl.writeLogEntry" );

        // Serialize to XML and write to stream
        writer.write( getXStream().toXML( entry ) );
    }


    private LogEntry readLogEntry( final BufferedReader reader )
    {
        try
        {
            // Deserialize next object on the stream
            return (LogEntry) getXStream().fromXML( reader );
        }
        catch ( BaseException e )
        {
            // Could not deserialize, assume we are out of objects?
            return null;
        }
    }


    private XStream getXStream()
    {
        if ( myXstream == null )
        {
            myXstream = new XStream();
        }

        return myXstream;
    }

}
